<?php

namespace Wonfu\Core;

/**
 * @author vison.cao visonforcoding@gmail.com
 */

use Ray\Aop\BindInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Wonfu\Core\Annotations\RequestMapping;
use DI\Container;
use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Wonfu\Core\Annotations\RequestTag;
use Wonfu\Core\Route\MethodNotAllowed;
use Wonfu\Core\Route\NotFoundException;
use Wonfu\Core\Route\RouteInfo;
use Wonfu\Http\ServerRequest;
use Wonfu\Http\ServerResponse;
use Wonfu\Reflections\MethodWrapper;
use Wonfu\Reflections\Reflections;
use function FastRoute\simpleDispatcher;
use Ray\Aop\Bind;
use Ray\Aop\Weaver;

abstract class AbstractApplication implements RequestHandlerInterface
{

    /**
     * @var Container
     */
    protected $container;

    /**
     * @var AppConfig
     */
    protected $appConfig;


    /**
     * @var ServerRequest
     */
    protected $request;


    /**
     * @var ServerResponse
     */
    protected $response;

    protected $exceptionHandler;


    /**
     * @return AppConfig
     */
    public function getAppConfig(): AppConfig
    {
        return $this->appConfig;
    }

    /**
     * @param AppConfig $appConfig
     */
    public function setAppConfig(AppConfig $appConfig): void
    {
        $this->appConfig = $appConfig;
    }


    /**
     * @param Container $container
     */
    public function setContainer(Container $container)
    {
        $this->container = $container;
        return $this;
    }


    /**
     * @return static
     */
    abstract public function boot(AppConfig $appConfig);

    /**
     * 处理请求 路由分发 返回 ServerRequestInterface
     * @param ServerRequest $request
     * @inheritDoc
     */
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $routeInfo = $this->dispatch($request);
        $request->setUniqReqNo(Utils::genrateUniReqNo());
        $this->request = $request;
        $response = new ServerResponse();
        $this->response = $response;
        switch ($routeInfo->getCode()) {
            case Dispatcher::NOT_FOUND:
                throw new NotFoundException("Route Not Found");
            case Dispatcher::METHOD_NOT_ALLOWED:
                throw new MethodNotAllowed("Method Not Allowed");
            case Dispatcher::FOUND:
                $handler = $routeInfo->getCallable();
                $this->container->set(ServerRequest::class, $request);
                $vars = $this->bindParams($handler, $routeInfo);
                $this->request->setController($handler[0]);
                $this->request->setAction($handler[1]);
                $bind = (new Bind())->bind($handler[0], $this->container->get('pointCut'));
                if ($bind->getBindings()) {
                    $compiler = new Weaver($bind, $this->appConfig->getTmpDir());
                    $handler[0] = $compiler->weave($handler[0]);
                    $instance = $this->container->get($handler[0]);
                    $instance->bindings = $bind->getBindings();
                    $this->aopInject($bind);
                    $body = call_user_func_array([$instance, $handler[1]], $vars);
                } else {
                    $body = $this->container->call($handler, $vars);
                }
                $this->bodyPrase($body);
                break;
        }
        return $this->response;
    }

    /**
     * aop method inject
     */
    private function aopInject(BindInterface $bind)
    {
        foreach ($bind->getBindings() as $aopInstances) {
            foreach ($aopInstances as $instance) {
                $this->container->injectOn($instance);
            }
        }
    }

    /**
     * @param mixed $body
     */
    private function bodyPrase($body)
    {
        if (is_string($body)) {
            return $this->response->setContent($body);
        }

        if (is_object($body)) {
            //Inject on an existing instance
            $this->container->injectOn($body);
            $encoders = [new XmlEncoder(), new JsonEncoder()];
            $normalizers = [new JsonSerializableNormalizer(),
                new ObjectNormalizer(null,new CamelCaseToSnakeCaseNameConverter(),null,new PropertyInfoExtractor()),
                ];
            $serializer = new Serializer($normalizers, $encoders);
            $jsonData = $serializer->serialize($body, 'json');
            $this->response->headers->set('Content-Type','application/json');
            return $this->response->setContent($jsonData);
        }
        return $this->response->setContent($body);
    }

    /**
     * @param ServerRequest $request
     * @return RouteInfo
     */
    public function dispatch(ServerRequestInterface $request): RouteInfo
    {
        $reflections = new Reflections();
        $methods = $reflections->include($this->appConfig->getControllerDir())
            ->finder()
            ->getMethodsWithAnnotation(RequestMapping::class, RequestTag::class);
        $dispatcher = simpleDispatcher(function (RouteCollector $r) use ($methods) {
            /**@var  MethodWrapper[] $methods */
            foreach ($methods as $method) {
                /** @var RequestMapping $requestMapping */
                $requestMapping = $method->getAnnotation();
                $r->addRoute($requestMapping->method, $requestMapping->path,
                    [$method->getReflectionMethod()->class, $method->getReflectionMethod()->name]);
            }
            // {id} must be a number (\d+)
        });
        $httpMethod = $request->getMethod();
        $uri = $request->getPathInfo();
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
        $routeInfoObj = new RouteInfo();
        $routeInfoObj->setCode($routeInfo[0]);
        if ($routeInfo[0] != Dispatcher::NOT_FOUND) {
            $routeInfoObj->setCallable($routeInfo[1]);
            $routeInfoObj->setParameter($routeInfo[2]);
        }
        return $routeInfoObj;
    }

    /**
     * @param $handler
     * @param RouteInfo $routeInfo
     * @return array
     * @throws \JsonMapper_Exception
     * @throws \ReflectionException
     */
    private function bindParams($handler, $routeInfo)
    {
        $methodReflection = new \ReflectionMethod($handler[0], $handler[1]);
        $paramsResolver = new ParamsResolver($methodReflection, $this->request, $routeInfo);
        return $paramsResolver->resolve();
    }

    public function run()
    {
        $request = ServerRequest::createFromGlobals();
        /** @var ServerResponse $response */
        $response = $this->handle($request);
        $response->send();
    }

}